/*
 * PhotoPanel.java 5 mai 2009
 *
 * Sweet Home 3D, Copyright (c) 2009 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.swing;

import java.awt.CardLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.imageio.ImageIO;
import javax.swing.ActionMap;
import javax.swing.DefaultListCellRenderer;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.KeyStroke;
import javax.swing.SpinnerDateModel;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.eteks.sweethome3d.j3d.PhotoRenderer;
import com.eteks.sweethome3d.model.AspectRatio;
import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.Camera.Lens;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.AbstractPhotoController;
import com.eteks.sweethome3d.viewcontroller.ContentManager;
import com.eteks.sweethome3d.viewcontroller.DialogView;
import com.eteks.sweethome3d.viewcontroller.Object3DFactory;
import com.eteks.sweethome3d.viewcontroller.PhotoController;
import com.eteks.sweethome3d.viewcontroller.View;

/**
 * A panel to edit photo creation. 
 * @author Emmanuel Puybaret
 */
public class PhotoPanel extends JPanel implements DialogView
{
	private enum ActionType
	{
		START_PHOTO_CREATION, STOP_PHOTO_CREATION, SAVE_PHOTO, CLOSE
	}
	
	private static final String PHOTO_DIALOG_X_VISUAL_PROPERTY = "com.eteks.sweethome3d.swing.PhotoPanel.PhotoDialogX";
	private static final String PHOTO_DIALOG_Y_VISUAL_PROPERTY = "com.eteks.sweethome3d.swing.PhotoPanel.PhotoDialogY";
	
	private static final int MINIMUM_DELAY_BEFORE_DISCARDING_WITHOUT_WARNING = 30000;
	
	private static final String WAIT_CARD = "wait";
	private static final String PHOTO_CARD = "photo";
	
	private final Home home;
	private final UserPreferences preferences;
	private final Object3DFactory object3dFactory;
	private final PhotoController controller;
	private ScaledImageComponent photoComponent;
	private JLabel animatedWaitLabel;
	private PhotoSizeAndQualityPanel sizeAndQualityPanel;
	private Component advancedComponentsSeparator;
	private JLabel dateLabel;
	private JSpinner dateSpinner;
	private JLabel timeLabel;
	private JSpinner timeSpinner;
	private JLabel dayNightLabel;
	private JLabel lensLabel;
	private JComboBox lensComboBox;
	private JCheckBox ceilingLightEnabledCheckBox;
	private String dialogTitle;
	private JPanel photoPanel;
	private CardLayout photoCardLayout;
	private ExecutorService photoCreationExecutor;
	private long photoCreationStartTime;
	private JButton createButton;
	private JButton saveButton;
	private JButton closeButton;
	
	private static PhotoPanel currentPhotoPanel; // Support only one photo panel opened at a time
	
	public PhotoPanel(Home home, UserPreferences preferences, PhotoController controller)
	{
		this(home, preferences, null, controller);
	}
	
	public PhotoPanel(Home home, UserPreferences preferences, Object3DFactory object3dFactory,
			PhotoController controller)
	{
		super(new GridBagLayout());
		this.home = home;
		this.preferences = preferences;
		this.object3dFactory = object3dFactory;
		this.controller = controller;
		createActions(preferences);
		createComponents(home, preferences, controller);
		setMnemonics(preferences);
		layoutComponents();
		
		preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, new LanguageChangeListener(this));
	}
	
	/**
	 * Creates actions for variables.
	 */
	private void createActions(UserPreferences preferences)
	{
		final ActionMap actions = getActionMap();
		actions.put(ActionType.START_PHOTO_CREATION,
				new ResourceAction(preferences, PhotoPanel.class, ActionType.START_PHOTO_CREATION.name(), true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						startPhotoCreation();
					}
				});
		actions.put(ActionType.STOP_PHOTO_CREATION,
				new ResourceAction(preferences, PhotoPanel.class, ActionType.STOP_PHOTO_CREATION.name(), true)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						stopPhotoCreation(true);
					}
				});
		actions.put(ActionType.SAVE_PHOTO,
				new ResourceAction(preferences, PhotoPanel.class, ActionType.SAVE_PHOTO.name(), false)
				{
					@Override
					public void actionPerformed(ActionEvent ev)
					{
						savePhoto();
					}
				});
		actions.put(ActionType.CLOSE, new ResourceAction(preferences, PhotoPanel.class, ActionType.CLOSE.name(), true)
		{
			@Override
			public void actionPerformed(ActionEvent ev)
			{
				close();
			}
		});
	}
	
	/**
	 * Creates and initializes components.
	 */
	private void createComponents(final Home home, final UserPreferences preferences, final PhotoController controller)
	{
		this.photoComponent = new ScaledImageComponent();
		this.photoComponent
				.setPreferredSize(new Dimension(getToolkit().getScreenSize().width <= 1024 ? 320 : 400, 400));
		// Under Mac OS X, set a transfer handler and a mouse listener on photo component 
		// to let the user drag and drop the created image (Windows support seems to fail) 
		if (OperatingSystem.isMacOSX() && !OperatingSystem.isJavaVersionBetween("1.7", "1.7.0_60"))
		{
			this.photoComponent.setTransferHandler(new VisualTransferHandler()
			{
				@Override
				public int getSourceActions(JComponent component)
				{
					return COPY_OR_MOVE;
				}
				
				@Override
				protected Transferable createTransferable(JComponent component)
				{
					return new Transferable()
					{
						public Object getTransferData(DataFlavor flavor)
						{
							return photoComponent.getImage();
						}
						
						public DataFlavor[] getTransferDataFlavors()
						{
							return new DataFlavor[] { DataFlavor.imageFlavor };
						}
						
						public boolean isDataFlavorSupported(DataFlavor flavor)
						{
							return flavor.equals(DataFlavor.imageFlavor);
						}
					};
				}
				
				@Override
				public Icon getVisualRepresentation(Transferable transferable)
				{
					try
					{
						if (transferable.isDataFlavorSupported(DataFlavor.imageFlavor))
						{
							// Create a 128x128 icon from the transfered image
							BufferedImage transferedImage = (BufferedImage) transferable
									.getTransferData(DataFlavor.imageFlavor);
							float scale = Math.min(1,
									128f / Math.max(transferedImage.getWidth(), transferedImage.getHeight()));
							BufferedImage iconImage = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB);
							Graphics2D g2D = (Graphics2D) iconImage.getGraphics();
							g2D.scale(scale, scale);
							g2D.drawRenderedImage(transferedImage, null);
							g2D.dispose();
							return new ImageIcon(iconImage);
						}
					}
					catch (UnsupportedFlavorException ex)
					{
						// Use default representation
					}
					catch (IOException ex)
					{
						// Use default representation
					}
					return super.getVisualRepresentation(transferable);
				}
			});
			this.photoComponent.addMouseListener(new MouseAdapter()
			{
				@Override
				public void mousePressed(MouseEvent ev)
				{
					if (SwingUtilities.isLeftMouseButton(ev) && photoComponent.getImage() != null
							&& photoComponent.isPointInImage(ev.getX(), ev.getY()))
					{
						photoComponent.getTransferHandler().exportAsDrag(photoComponent, ev, TransferHandler.COPY);
					}
				}
			});
		}
		
		this.animatedWaitLabel = new JLabel(new ImageIcon(PhotoPanel.class.getResource("resources/animatedWait.gif")));
		
		// Create size and quality panel
		this.sizeAndQualityPanel = new PhotoSizeAndQualityPanel(home, preferences, controller);
		controller.addPropertyChangeListener(AbstractPhotoController.Property.QUALITY, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				updateAdvancedComponents();
			}
		});
		
		this.advancedComponentsSeparator = new JSeparator();
		
		// Create date and time labels and spinners bound to TIME controller property
		Date time = new Date(Camera.convertTimeToTimeZone(controller.getTime(), TimeZone.getDefault().getID()));
		this.dateLabel = new JLabel();
		final SpinnerDateModel dateSpinnerModel = new SpinnerDateModel();
		dateSpinnerModel.setValue(time);
		this.dateSpinner = new JSpinner(dateSpinnerModel);
		String datePattern = ((SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT)).toPattern();
		if (datePattern.indexOf("yyyy") == -1)
		{
			datePattern = datePattern.replace("yy", "yyyy");
		}
		JSpinner.DateEditor dateEditor = new JSpinner.DateEditor(this.dateSpinner, datePattern);
		this.dateSpinner.setEditor(dateEditor);
		SwingTools.addAutoSelectionOnFocusGain(dateEditor.getTextField());
		
		this.timeLabel = new JLabel();
		final SpinnerDateModel timeSpinnerModel = new SpinnerDateModel();
		timeSpinnerModel.setValue(time);
		this.timeSpinner = new JSpinner(timeSpinnerModel);
		// From http://en.wikipedia.org/wiki/12-hour_clock#Use_by_country
		String[] twelveHoursCountries = { "AU", // Australia
				"BD", // Bangladesh
				"CA", // Canada (excluding Quebec, in French)
				"CO", // Colombia
				"EG", // Egypt
				"HN", // Honduras
				"JO", // Jordan
				"MX", // Mexico
				"MY", // Malaysia
				"NI", // Nicaragua
				"NZ", // New Zealand
				"PH", // Philippines
				"PK", // Pakistan
				"SA", // Saudi Arabia
				"SV", // El Salvador
				"US", // United States
				"VE" }; // Venezuela         
		SimpleDateFormat timeInstance;
		if ("en".equals(Locale.getDefault().getLanguage()))
		{
			if (Arrays.binarySearch(twelveHoursCountries, Locale.getDefault().getCountry()) >= 0)
			{
				timeInstance = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.SHORT, Locale.US); // 12 hours notation
			}
			else
			{
				timeInstance = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.SHORT, Locale.UK); // 24 hours notation
			}
		}
		else
		{
			timeInstance = (SimpleDateFormat) DateFormat.getTimeInstance(DateFormat.SHORT);
		}
		JSpinner.DateEditor timeEditor = new JSpinner.DateEditor(this.timeSpinner, timeInstance.toPattern());
		this.timeSpinner.setEditor(timeEditor);
		SwingTools.addAutoSelectionOnFocusGain(timeEditor.getTextField());
		
		final PropertyChangeListener timeChangeListener = new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				Date date = new Date(Camera.convertTimeToTimeZone(controller.getTime(), TimeZone.getDefault().getID()));
				dateSpinnerModel.setValue(date);
				timeSpinnerModel.setValue(date);
			}
		};
		controller.addPropertyChangeListener(PhotoController.Property.TIME, timeChangeListener);
		final ChangeListener dateTimeChangeListener = new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				controller.removePropertyChangeListener(PhotoController.Property.TIME, timeChangeListener);
				// Merge date and time
				GregorianCalendar dateCalendar = new GregorianCalendar();
				dateCalendar.setTime((Date) dateSpinnerModel.getValue());
				GregorianCalendar timeCalendar = new GregorianCalendar();
				timeCalendar.setTime((Date) timeSpinnerModel.getValue());
				Calendar utcCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
				utcCalendar.set(GregorianCalendar.YEAR, dateCalendar.get(GregorianCalendar.YEAR));
				utcCalendar.set(GregorianCalendar.MONTH, dateCalendar.get(GregorianCalendar.MONTH));
				utcCalendar.set(GregorianCalendar.DAY_OF_MONTH, dateCalendar.get(GregorianCalendar.DAY_OF_MONTH));
				utcCalendar.set(GregorianCalendar.HOUR_OF_DAY, timeCalendar.get(GregorianCalendar.HOUR_OF_DAY));
				utcCalendar.set(GregorianCalendar.MINUTE, timeCalendar.get(GregorianCalendar.MINUTE));
				utcCalendar.set(GregorianCalendar.SECOND, timeCalendar.get(GregorianCalendar.SECOND));
				controller.setTime(utcCalendar.getTimeInMillis());
				controller.addPropertyChangeListener(PhotoController.Property.TIME, timeChangeListener);
			}
		};
		dateSpinnerModel.addChangeListener(dateTimeChangeListener);
		timeSpinnerModel.addChangeListener(dateTimeChangeListener);
		
		this.dayNightLabel = new JLabel();
		final ImageIcon dayIcon = SwingTools.getScaledImageIcon(PhotoPanel.class.getResource("resources/day.png"));
		final ImageIcon nightIcon = SwingTools.getScaledImageIcon(PhotoPanel.class.getResource("resources/night.png"));
		PropertyChangeListener dayNightListener = new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				if (home.getCompass().getSunElevation(
						Camera.convertTimeToTimeZone(controller.getTime(), home.getCompass().getTimeZone())) > 0)
				{
					dayNightLabel.setIcon(dayIcon);
				}
				else
				{
					dayNightLabel.setIcon(nightIcon);
				}
			}
		};
		controller.addPropertyChangeListener(PhotoController.Property.TIME, dayNightListener);
		home.getCompass().addPropertyChangeListener(dayNightListener);
		dayNightListener.propertyChange(null);
		
		// Create lens label and combo box
		this.lensLabel = new JLabel();
		this.lensComboBox = new JComboBox(Camera.Lens.values());
		this.lensComboBox.setRenderer(new DefaultListCellRenderer()
		{
			@Override
			public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
					boolean cellHasFocus)
			{
				String displayedValue;
				switch ((Camera.Lens) value)
				{
					case NORMAL:
						displayedValue = preferences.getLocalizedString(PhotoPanel.class,
								"lensComboBox.normalLens.text");
						break;
					case SPHERICAL:
						displayedValue = preferences.getLocalizedString(PhotoPanel.class,
								"lensComboBox.sphericalLens.text");
						break;
					case FISHEYE:
						displayedValue = preferences.getLocalizedString(PhotoPanel.class,
								"lensComboBox.fisheyeLens.text");
						break;
					case PINHOLE:
					default:
						displayedValue = preferences.getLocalizedString(PhotoPanel.class,
								"lensComboBox.pinholeLens.text");
						break;
				}
				return super.getListCellRendererComponent(list, displayedValue, index, isSelected, cellHasFocus);
			}
		});
		this.lensComboBox.setSelectedItem(controller.getLens());
		controller.addPropertyChangeListener(PhotoController.Property.LENS, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				lensComboBox.setSelectedItem(controller.getLens());
			}
		});
		this.lensComboBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				Camera.Lens lens = (Camera.Lens) lensComboBox.getSelectedItem();
				controller.setLens(lens);
				if (lens == Camera.Lens.SPHERICAL)
				{
					controller.setAspectRatio(AspectRatio.RATIO_2_1);
				}
				else if (lens == Camera.Lens.FISHEYE)
				{
					controller.setAspectRatio(AspectRatio.SQUARE_RATIO);
				}
				updateRatioComponents();
			}
		});
		
		this.ceilingLightEnabledCheckBox = new JCheckBox();
		this.ceilingLightEnabledCheckBox.setSelected(controller.getCeilingLightColor() > 0);
		controller.addPropertyChangeListener(AbstractPhotoController.Property.CEILING_LIGHT_COLOR,
				new PropertyChangeListener()
				{
					public void propertyChange(PropertyChangeEvent ev)
					{
						ceilingLightEnabledCheckBox.setSelected(controller.getCeilingLightColor() > 0);
					}
				});
		this.ceilingLightEnabledCheckBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setCeilingLightColor(ceilingLightEnabledCheckBox.isSelected() ? 0xD0D0D0 : 0);
			}
		});
		
		final JComponent view3D = (JComponent) controller.get3DView();
		controller.set3DViewAspectRatio((float) view3D.getWidth() / view3D.getHeight());
		
		final ActionMap actionMap = getActionMap();
		this.createButton = new JButton(actionMap.get(ActionType.START_PHOTO_CREATION));
		this.saveButton = new JButton(actionMap.get(ActionType.SAVE_PHOTO));
		this.closeButton = new JButton(actionMap.get(ActionType.CLOSE));
		
		setComponentTexts(preferences);
		updateRatioComponents();
	}
	
	/**
	 * Sets the texts of the components.
	 */
	private void setComponentTexts(UserPreferences preferences)
	{
		this.dateLabel.setText(SwingTools.getLocalizedLabelText(preferences, PhotoPanel.class, "dateLabel.text"));
		this.timeLabel.setText(SwingTools.getLocalizedLabelText(preferences, PhotoPanel.class, "timeLabel.text"));
		this.lensLabel.setText(SwingTools.getLocalizedLabelText(preferences, PhotoPanel.class, "lensLabel.text"));
		this.ceilingLightEnabledCheckBox.setText(
				SwingTools.getLocalizedLabelText(preferences, PhotoPanel.class, "ceilingLightEnabledCheckBox.text"));
		this.dialogTitle = preferences.getLocalizedString(PhotoPanel.class, "createPhoto.title");
		Window window = SwingUtilities.getWindowAncestor(this);
		if (window != null)
		{
			((JDialog) window).setTitle(this.dialogTitle);
		}
		// Buttons text changes automatically through their action
	}
	
	/**
	 * Sets components mnemonics and label / component associations.
	 */
	private void setMnemonics(UserPreferences preferences)
	{
		if (!OperatingSystem.isMacOSX())
		{
			this.dateLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PhotoPanel.class, "dateLabel.mnemonic")).getKeyCode());
			this.dateLabel.setLabelFor(this.dateSpinner);
			this.timeLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PhotoPanel.class, "timeLabel.mnemonic")).getKeyCode());
			this.timeLabel.setLabelFor(this.timeSpinner);
			this.lensLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PhotoPanel.class, "lensLabel.mnemonic")).getKeyCode());
			this.lensLabel.setLabelFor(this.lensComboBox);
			this.ceilingLightEnabledCheckBox.setMnemonic(KeyStroke
					.getKeyStroke(
							preferences.getLocalizedString(PhotoPanel.class, "ceilingLightEnabledCheckBox.mnemonic"))
					.getKeyCode());
		}
	}
	
	/**
	 * Preferences property listener bound to this panel with a weak reference to avoid
	 * strong link between user preferences and this panel.  
	 */
	public static class LanguageChangeListener implements PropertyChangeListener
	{
		private final WeakReference<PhotoPanel> photoPanel;
		
		public LanguageChangeListener(PhotoPanel photoPanel)
		{
			this.photoPanel = new WeakReference<PhotoPanel>(photoPanel);
		}
		
		public void propertyChange(PropertyChangeEvent ev)
		{
			// If photo panel was garbage collected, remove this listener from preferences
			PhotoPanel photoPanel = this.photoPanel.get();
			UserPreferences preferences = (UserPreferences) ev.getSource();
			if (photoPanel == null)
			{
				preferences.removePropertyChangeListener(UserPreferences.Property.LANGUAGE, this);
			}
			else
			{
				photoPanel.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
				photoPanel.setComponentTexts(preferences);
				photoPanel.setMnemonics(preferences);
			}
		}
	}
	
	/**
	 * Layouts panel components in panel with their labels. 
	 */
	private void layoutComponents()
	{
		int labelAlignment = OperatingSystem.isMacOSX() ? JLabel.TRAILING : JLabel.LEADING;
		// Add animatedWaitLabel and photoComponent to a card panel 
		this.photoCardLayout = new CardLayout();
		this.photoPanel = new JPanel(this.photoCardLayout);
		photoPanel.add(this.photoComponent, PHOTO_CARD);
		photoPanel.add(this.animatedWaitLabel, WAIT_CARD);
		// First row
		add(this.photoPanel, new GridBagConstraints(0, 0, 3, 1, 1, 1, GridBagConstraints.CENTER,
				GridBagConstraints.BOTH, new Insets(0, 0, 5, 0), 0, 0));
		// Second row
		// Add a dummy label at left and right
		add(new JLabel(), new GridBagConstraints(0, 1, 1, 8, 0.5f, 0, GridBagConstraints.CENTER,
				GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
		add(new JLabel(), new GridBagConstraints(2, 1, 1, 8, 0.5f, 0, GridBagConstraints.CENTER,
				GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
		add(this.sizeAndQualityPanel, new GridBagConstraints(1, 1, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
		// Third row
		add(this.advancedComponentsSeparator, new GridBagConstraints(1, 2, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(3, 0, 3, 0), 0, 0));
		// Forth row
		JPanel advancedPanel = new JPanel(new GridBagLayout());
		advancedPanel.add(this.dateLabel, new GridBagConstraints(0, 1, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 5), 0, 0));
		this.dateLabel.setHorizontalAlignment(labelAlignment);
		advancedPanel.add(this.dateSpinner, new GridBagConstraints(1, 1, 1, 1, 1, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 10), 0, 0));
		advancedPanel.add(this.timeLabel, new GridBagConstraints(2, 1, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 5), 0, 0));
		this.timeLabel.setHorizontalAlignment(labelAlignment);
		advancedPanel.add(this.timeSpinner, new GridBagConstraints(3, 1, 1, 1, 1, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 5, 5), 0, 0));
		advancedPanel.add(this.dayNightLabel, new GridBagConstraints(4, 1, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
		// Last row
		advancedPanel.add(this.lensLabel, new GridBagConstraints(0, 2, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 5), 0, 0));
		this.lensLabel.setHorizontalAlignment(labelAlignment);
		advancedPanel.add(this.lensComboBox, new GridBagConstraints(1, 2, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 10), 0, 0));
		advancedPanel.add(this.ceilingLightEnabledCheckBox, new GridBagConstraints(2, 2, 3, 1, 0, 0,
				GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
		add(advancedPanel, new GridBagConstraints(1, 3, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
	}
	
	private void updateAdvancedComponents()
	{
		Component root = SwingUtilities.getRoot(this);
		if (root != null)
		{
			boolean highQuality = controller.getQuality() >= 2;
			boolean advancedComponentsVisible = this.advancedComponentsSeparator.isVisible();
			if (advancedComponentsVisible != highQuality)
			{
				setAdvancedComponentsVisible(highQuality);
				updateRatioComponents();
				int componentsHeight = this.advancedComponentsSeparator.getPreferredSize().height + 6
						+ this.dateSpinner.getPreferredSize().height + 5 + this.lensComboBox.getPreferredSize().height;
				root.setSize(root.getWidth(),
						root.getHeight() + (advancedComponentsVisible ? -componentsHeight : componentsHeight));
			}
		}
	}
	
	private void setAdvancedComponentsVisible(boolean visible)
	{
		this.advancedComponentsSeparator.setVisible(visible);
		this.dateLabel.setVisible(visible);
		this.dateSpinner.setVisible(visible);
		this.timeLabel.setVisible(visible);
		this.timeSpinner.setVisible(visible);
		this.dayNightLabel.setVisible(visible);
		this.lensLabel.setVisible(visible);
		this.lensComboBox.setVisible(visible);
		this.ceilingLightEnabledCheckBox.setVisible(visible);
	}
	
	/**
	 * Updates photo height.  
	 */
	private void updateRatioComponents()
	{
		Lens lens = this.controller.getLens();
		boolean fixedProportions = this.lensComboBox.isVisible()
				&& (lens == Camera.Lens.FISHEYE || lens == Camera.Lens.SPHERICAL);
		this.sizeAndQualityPanel.setProportionsChoiceEnabled(!fixedProportions);
	}
	
	/**
	 * Displays this panel in a non modal dialog.
	 */
	public void displayView(View parentView)
	{
		if (currentPhotoPanel == this)
		{
			SwingUtilities.getWindowAncestor(PhotoPanel.this).toFront();
		}
		else
		{
			if (currentPhotoPanel != null)
			{
				currentPhotoPanel.close();
			}
			final JOptionPane optionPane = new JOptionPane(this, JOptionPane.PLAIN_MESSAGE,
					JOptionPane.OK_CANCEL_OPTION, null,
					new Object[] { this.createButton, this.saveButton, this.closeButton }, this.createButton);
			if (parentView != null)
			{
				optionPane.setComponentOrientation(((JComponent) parentView).getComponentOrientation());
			}
			final JDialog dialog = optionPane.createDialog(SwingUtilities.getRootPane((Component) parentView),
					this.dialogTitle);
			dialog.setModal(false);
			
			Component homeRoot = SwingUtilities.getRoot((Component) parentView);
			Point dialogLocation = null;
			if (homeRoot != null)
			{
				// Restore location if it exists
				Number x = this.home.getNumericProperty(PHOTO_DIALOG_X_VISUAL_PROPERTY);
				Number y = this.home.getNumericProperty(PHOTO_DIALOG_Y_VISUAL_PROPERTY);
				
				int windowRightBorder = homeRoot.getX() + homeRoot.getWidth();
				Dimension screenSize = getToolkit().getScreenSize();
				Insets screenInsets = getToolkit().getScreenInsets(getGraphicsConfiguration());
				int screenRightBorder = screenSize.width - screenInsets.right;
				// Check dialog isn't too high
				int screenHeight = screenSize.height - screenInsets.top - screenInsets.bottom;
				if (OperatingSystem.isLinux() && screenHeight == screenSize.height)
				{
					// Let's consider that under Linux at least an horizontal bar exists 
					screenHeight -= 30;
				}
				int screenBottomBorder = screenSize.height - screenInsets.bottom;
				int dialogWidth = dialog.getWidth();
				if (dialog.getHeight() > screenHeight)
				{
					dialog.setSize(dialogWidth, screenHeight);
				}
				int dialogHeight = dialog.getHeight();
				if (x != null && y != null && x.intValue() + dialogWidth <= screenRightBorder
						&& y.intValue() + dialogHeight <= screenBottomBorder)
				{
					dialogLocation = new Point(x.intValue(), y.intValue());
				}
				else if (screenRightBorder - windowRightBorder > dialogWidth / 2 || dialogHeight == screenHeight)
				{
					// If there some space left at the right of the window,
					// move the dialog to the right of window
					dialogLocation = new Point(Math.min(windowRightBorder + 5, screenRightBorder - dialogWidth),
							Math.max(Math.min(homeRoot.getY(), screenSize.height - dialogHeight - screenInsets.bottom),
									screenInsets.top));
				}
			}
			if (dialogLocation != null
					&& SwingTools.isRectangleVisibleAtScreen(new Rectangle(dialogLocation, dialog.getSize())))
			{
				dialog.setLocationByPlatform(false);
				dialog.setLocation(dialogLocation);
			}
			else
			{
				dialog.setLocationByPlatform(true);
			}
			
			dialog.addWindowListener(new WindowAdapter()
			{
				public void windowClosed(WindowEvent ev)
				{
					stopPhotoCreation(false);
					currentPhotoPanel = null;
				}
			});
			
			updateAdvancedComponents();
			dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
			dialog.addComponentListener(new ComponentAdapter()
			{
				@Override
				public void componentHidden(ComponentEvent ev)
				{
					if (optionPane.getValue() != null && optionPane.getValue() != JOptionPane.UNINITIALIZED_VALUE)
					{
						close();
					}
				}
				
				@Override
				public void componentMoved(ComponentEvent ev)
				{
					controller.setHomeProperty(PHOTO_DIALOG_X_VISUAL_PROPERTY, String.valueOf(dialog.getX()));
					controller.setHomeProperty(PHOTO_DIALOG_Y_VISUAL_PROPERTY, String.valueOf(dialog.getY()));
				}
			});
			dialog.setVisible(true);
			currentPhotoPanel = this;
		}
	}
	
	/**
	 * Creates the photo image depending on the quality requested by the user.
	 */
	private void startPhotoCreation()
	{
		this.photoComponent.setImage(null);
		this.sizeAndQualityPanel.setEnabled(false);
		this.dateSpinner.setEnabled(false);
		this.timeSpinner.setEnabled(false);
		this.lensComboBox.setEnabled(false);
		this.ceilingLightEnabledCheckBox.setEnabled(false);
		getActionMap().get(ActionType.SAVE_PHOTO).setEnabled(false);
		getRootPane().setDefaultButton(this.createButton);
		this.createButton.setAction(getActionMap().get(ActionType.STOP_PHOTO_CREATION));
		this.photoCardLayout.show(this.photoPanel, WAIT_CARD);
		
		// Compute photo in an other executor thread
		// Use a clone of home because the user can modify home during photo computation
		final Home home = this.home.clone();
		List<Selectable> emptySelection = Collections.emptyList();
		home.setSelectedItems(emptySelection);
		this.photoCreationExecutor = Executors.newSingleThreadExecutor();
		this.photoCreationExecutor.execute(new Runnable()
		{
			public void run()
			{
				computePhoto(home);
			}
		});
	}
	
	/**
	 * Computes the photo of the given home.
	 * Caution : this method must be thread safe because it's called from an executor. 
	 */
	private void computePhoto(Home home)
	{
		this.photoCreationStartTime = System.currentTimeMillis();
		BufferedImage image = null;
		try
		{
			int quality = this.controller.getQuality();
			int imageWidth = this.controller.getWidth();
			int imageHeight = this.controller.getHeight();
			if (quality >= 2)
			{
				// Use photo renderer
				PhotoRenderer photoRenderer = new PhotoRenderer(home, this.object3dFactory,
						quality == 2 ? PhotoRenderer.Quality.LOW : PhotoRenderer.Quality.HIGH);
				int bestImageHeight;
				// Check correct ratio if lens is fisheye or spherical
				Camera camera = home.getCamera();
				if (camera.getLens() == Camera.Lens.FISHEYE)
				{
					bestImageHeight = imageWidth;
				}
				else if (camera.getLens() == Camera.Lens.SPHERICAL)
				{
					bestImageHeight = imageWidth / 2;
				}
				else
				{
					bestImageHeight = imageHeight;
				}
				if (photoCreationExecutor != null)
				{
					image = new BufferedImage(imageWidth, bestImageHeight, BufferedImage.TYPE_INT_RGB);
					this.photoComponent.setImage(image);
					EventQueue.invokeLater(new Runnable()
					{
						public void run()
						{
							photoCardLayout.show(photoPanel, PHOTO_CARD);
						}
					});
					photoRenderer.render(image, camera, this.photoComponent);
					photoRenderer.dispose();
				}
			}
			else
			{
				// Compute 3D view offscreen image
				HomeComponent3D homeComponent3D = new HomeComponent3D(home, this.preferences, this.object3dFactory,
						quality == 1, null);
				image = homeComponent3D.getOffScreenImage(imageWidth, imageHeight);
			}
		}
		catch (OutOfMemoryError ex)
		{
			image = getErrorImage();
			throw ex;
		}
		catch (IllegalStateException ex)
		{
			image = getErrorImage();
			throw ex;
		}
		catch (IOException ex)
		{
			image = getErrorImage();
		}
		finally
		{
			final BufferedImage photoImage = this.photoCreationExecutor != null ? image : null;
			EventQueue.invokeLater(new Runnable()
			{
				public void run()
				{
					getActionMap().get(ActionType.SAVE_PHOTO).setEnabled(photoImage != null);
					if (photoImage != null)
					{
						getRootPane().setDefaultButton(saveButton);
					}
					createButton.setAction(getActionMap().get(ActionType.START_PHOTO_CREATION));
					photoComponent.setImage(photoImage);
					sizeAndQualityPanel.setEnabled(true);
					updateRatioComponents();
					dateSpinner.setEnabled(true);
					timeSpinner.setEnabled(true);
					lensComboBox.setEnabled(true);
					ceilingLightEnabledCheckBox.setEnabled(true);
					photoCardLayout.show(photoPanel, PHOTO_CARD);
					photoCreationExecutor = null;
				}
			});
		}
	}
	
	/**
	 * Returns the image used in case of an error.
	 */
	private BufferedImage getErrorImage()
	{
		Icon errorIcon = IconManager.getInstance().getErrorIcon(16);
		BufferedImage errorImage = new BufferedImage(errorIcon.getIconWidth(), errorIcon.getIconHeight(),
				BufferedImage.TYPE_INT_ARGB);
		Graphics2D g2D = (Graphics2D) errorImage.getGraphics();
		errorIcon.paintIcon(this, g2D, 0, 0);
		g2D.dispose();
		return errorImage;
	}
	
	/**
	 * Stops photo creation.
	 */
	private void stopPhotoCreation(boolean confirmStop)
	{
		if (this.photoCreationExecutor != null
				// Confirm the stop if a rendering has been running for more than 30 s 
				&& (!confirmStop
						|| System.currentTimeMillis()
								- this.photoCreationStartTime < MINIMUM_DELAY_BEFORE_DISCARDING_WITHOUT_WARNING
						|| JOptionPane.showConfirmDialog(getRootPane(),
								this.preferences.getLocalizedString(PhotoPanel.class, "confirmStopCreation.message"),
								this.preferences.getLocalizedString(PhotoPanel.class, "confirmStopCreation.title"),
								JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION))
		{
			if (this.photoCreationExecutor != null)
			{ // Check a second time in case rendering stopped meanwhile
				// Will interrupt executor thread      
				this.photoCreationExecutor.shutdownNow();
				this.photoCreationExecutor = null;
				this.createButton.setAction(getActionMap().get(ActionType.START_PHOTO_CREATION));
			}
		}
	}
	
	/**
	 * Saves the created image.
	 */
	private void savePhoto()
	{
		String pngFile = this.controller.getContentManager().showSaveDialog(this,
				this.preferences.getLocalizedString(PhotoPanel.class, "savePhotoDialog.title"),
				ContentManager.ContentType.PNG, this.home.getName());
		try
		{
			if (pngFile != null)
			{
				ImageIO.write(this.photoComponent.getImage(), "PNG", new File(pngFile));
			}
		}
		catch (IOException ex)
		{
			String messageFormat = this.preferences.getLocalizedString(PhotoPanel.class, "savePhotoError.message");
			JOptionPane.showMessageDialog(SwingUtilities.getRootPane(this),
					String.format(messageFormat, ex.getMessage()),
					this.preferences.getLocalizedString(PhotoPanel.class, "savePhotoError.title"),
					JOptionPane.ERROR_MESSAGE);
		}
	}
	
	/**
	 * Manages closing of this pane.
	 */
	private void close()
	{
		Window window = SwingUtilities.getWindowAncestor(this);
		if (window.isDisplayable())
		{
			window.dispose();
		}
	}
}
